/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2008 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Galaxium.Protocol
{
	public class SocksSocket
	{
		private SocksSocket ()
		{
			
		}
		
		private static string[] errorMsgs = {
			"Operation completed successfully.",
			"General SOCKS server failure.",
			"Connection not allowed by ruleset.",
			"Network unreachable.",
			"Host unreachable.",
			"Connection refused.",
			"TTL expired.",
			"Command not supported.",
			"Address type not supported.",
			"Unknown error."
		};
		
		public static Socket ConnectToSocks5Proxy (string proxyAdress, ushort proxyPort, string destAddress, ushort destPort, string userName, string password)
		{
			IPAddress destIP = null;
			IPAddress proxyIP = null;
			byte[] request = new byte[257];
			byte[] response = new byte[257];
			ushort nIndex;
			
			try
			{
				proxyIP =  IPAddress.Parse (proxyAdress);
			}
			catch (FormatException)
			{
				proxyIP = Dns.GetHostByAddress (proxyAdress).AddressList[0];
			}
			
			try
			{
				destIP = IPAddress.Parse (destAddress);
			}
			catch (FormatException)
			{
				//destIP = Dns.GetHostByAddress (destAddress).AddressList[0];
			}
			
			IPEndPoint proxyEndPoint = new IPEndPoint (proxyIP, proxyPort);
			
			Socket socksSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			
			try
			{
				socksSocket.Connect (proxyEndPoint);
			}
			catch (SocketException ex)
			{
				throw new ProxyConnectionException ("Proxy: "+ex.Message);
			}
			
			nIndex = 0;
			request[nIndex++]=0x05; // Version 5
			request[nIndex++]=0x02; // We support 2 Authentication methods
			request[nIndex++]=0x00; // No Auth
			request[nIndex++]=0x02; // User/Pass Auth
			
			socksSocket.Send (request, nIndex, SocketFlags.None);
			
			int bytesReceived = socksSocket.Receive (response, 2, SocketFlags.None);
			if (bytesReceived != 2)
				throw new ProxyConnectionException ("Bad response received from proxy server.");
			
			if (response[1] == 0xFF)
			{
				// No authentication method was accepted
				socksSocket.Close();
				throw new ProxyConnectionException ("None of the authentication method was accepted by proxy server.");
			}
			
			byte[] rawBytes;
			
			if (response[1] == 0x02) //Username/Password Authentication protocol
			{
				nIndex = 0;
				request[nIndex++] = 0x05; // Version 5.
				
				// add user name
				request[nIndex++] = (byte)userName.Length;
				rawBytes = Encoding.Default.GetBytes (userName);
				rawBytes.CopyTo (request, nIndex);
				nIndex += (ushort)rawBytes.Length;
				
				// add password
				request[nIndex++] = (byte)password.Length;
				rawBytes = Encoding.Default.GetBytes (password);
				rawBytes.CopyTo (request,nIndex);
				nIndex += (ushort)rawBytes.Length;
				
				// Send the Username/Password request
				socksSocket.Send(request,nIndex,SocketFlags.None);
				
				// Receive 2 byte response...
				bytesReceived = socksSocket.Receive (response,2,SocketFlags.None);
				
				if (bytesReceived!=2)
					throw new ProxyConnectionException ("Bad response received from proxy server.");
				
				if (response[1] != 0x00)
					throw new ProxyConnectionException ("Bad Username/Password.");
			}
			
			// Send connect request now...
			nIndex = 0;
			request[nIndex++]=0x05; // version 5.
			request[nIndex++]=0x01; // command = connect.
			request[nIndex++]=0x00; // Reserve = must be 0x00
			
			if (destIP != null) // Destination adress in an IP.
			{
				switch(destIP.AddressFamily)
				{
					case AddressFamily.InterNetwork:
						// Address is IPV4 format
						request[nIndex++]=0x01;
						rawBytes = destIP.GetAddressBytes();
						rawBytes.CopyTo(request,nIndex);
						nIndex+=(ushort)rawBytes.Length;
						break;
					case AddressFamily.InterNetworkV6:
						// Address is IPV6 format
						request[nIndex++]=0x04;
						rawBytes = destIP.GetAddressBytes();
						rawBytes.CopyTo(request,nIndex);
						nIndex+=(ushort)rawBytes.Length;
						break;
				}
			}
			else
			{// Dest. address is domain name.
				request[nIndex++]=0x03;	// Address is full-qualified domain name.
				request[nIndex++]=Convert.ToByte(destAddress.Length); // length of address.
				rawBytes = Encoding.Default.GetBytes(destAddress);
				rawBytes.CopyTo(request,nIndex);
				nIndex+=(ushort)rawBytes.Length;
			}
			
			// using big-edian byte order
			byte[] portBytes = BitConverter.GetBytes(destPort);
			for (int i=portBytes.Length-1;i>=0;i--)
				request[nIndex++]=portBytes[i];
			
			// send connect request.
			socksSocket.Send(request,nIndex,SocketFlags.None);
			
			socksSocket.Receive(response); // Get variable length response...
			
			if (response[1]!=0x00)
				throw new ProxyConnectionException (errorMsgs[response[1]]);
			
			Console.WriteLine ("PROXY: Successfully connected through proxy connection!");
			
			return socksSocket;
		}
	}
}